/**
 * 
 */
package com.devmindset.jpatoolbox;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;

import org.apache.log4j.Logger;

/**
 * <pre>
 * Utility class to wire the bidirectionality of a Many to One/One to Many relationship.
 * 
 * The wiring is done according to the following rules and conventions:
 * 
 * Use case: Customer <-> Invoice.
 * 
 * For this relationship, we can say that one customer can have multiple invoices,
 * but one invoice can refer only to one customer.
 * 
 * Also, from the business logic perspective, it makes sense to have relationship
 * from the customer to the invoice and also to be able to lookup the customer for
 * the given invoice, hence classifying the relationship as bidirectional.
 * 
 * We assume the following:
 * 
 * The unary (single) side of the relationship has only a setter, and the multi
 * side has set, add and remove methods. For example:
 * 
 * The <code>Invoice</code> entity has <code>setCustomer(Customer customer)</code> method, while
 * the M side entity, <code>Customer</code> has <code>setInvoices(List<Invoice> invoice)</code>,
 * <code>addInvoice(Invoice invoice)</code> and <code>removeInvoice(Invoice invoice)</code> methods.
 * 
 * To wire the sides you use the methods of this class; see the specific methods for usage details.
 * </pre>
 * 
 * @author <a href="mailto:ice.penov@devmindset.com">Ice Penov</a>
 */
@SuppressWarnings("unchecked")
public final class OneToManyUtil {
  
  private static final Logger logger = Logger.getLogger(OneToManyUtil.class);
  
  private OneToManyUtil() {
  }
  
  /**
   * <pre> 
   * Wires the unary (single) side of a One to Many relationship,
   * by means of association.
   * 
   * For the Customer <-> Invoice use case, you use this method in the setter
   * on the unary entity, in this case:
   * 
   * In the Invoice class, on the Customer entity:
   * 
   * public void setCustomer(Customer customer) {
   *   OneToManyUtil.wireUnarySide(this, customer, Invoice.class, Customer.class);
   * }
   * </pre>
   * 
   * @param thisSide this side of the 1 to M relationship
   * @param otherSide the other side of the 1 to M relationship
   * @param thisSideClass the class on this side of the relationship
   * @param otherSideClass the class on the other side of the relationship
   */
  public static void wireUnarySide(Object thisSide, Object otherSide, Class thisSideClass, Class otherSideClass) {
    try {
      Method removeThisSide = otherSideClass.getMethod(Composer.composeRemover(thisSideClass), thisSideClass);
      Method addThisSide = otherSideClass.getMethod(Composer.composeAdder(thisSideClass), thisSideClass);
      Field OtherSideField = thisSideClass.getDeclaredField(Composer.getFieldName(otherSideClass));      
      OtherSideField.setAccessible(true);      
      Method getOtherSide = thisSideClass.getMethod(Composer.composeGetter(otherSideClass), (Class[]) null);      
      if (otherSide == null) {
        removeThisSide.invoke(getOtherSide.invoke(thisSide, (Object[]) null), thisSide);
        OtherSideField.set(thisSide, null);
      } else if (!otherSide.equals(getOtherSide.invoke(thisSide, (Object[]) null))) {
        //clear connections, if any.
        if (getOtherSide.invoke(thisSide, (Object[]) null) != null) {
          removeThisSide.invoke(getOtherSide.invoke(thisSide, (Object[]) null), thisSide);
        }
        OtherSideField.set(thisSide, otherSide);
        addThisSide.invoke(getOtherSide.invoke(thisSide, (Object[]) null), thisSide);
      }
    } catch (Exception e) {
      logger.error("Error while wiring the single side on a 1 to M bidirectional relationship", e);
    }
  }
  
  /**
   * </pre>
   * Wires the Many to One bidirectional relationship, by means of association.
   * 
   * For the Customer <-> Invoice use case, you use this method in the setter
   * on the M sided entity, in this case:
   * 
   * In the Customer class:
   * 
   * public void setInvoices(List<Invoice> invoices) {
   *   OneToManyUtil.wireCollection(this, invoices, Customer.class, Invoice.class);
   * }
   * </pre>
   * 
   * @param thisSide this side of the M to 1 relationship
   * @param otherSide the other side of the M to 1 relationship
   * @param thisSideClass the class on this side of the relationship
   * @param otherSideClass the class on the other side of the relationship
   */
  public static void wireCollection(Object thisSide, List otherSide, Class thisSideClass, Class otherSideClass) {
    try {
      Field localOtherSideField = thisSideClass.getDeclaredField(Composer.composeCollectionFieldName(otherSideClass));
      localOtherSideField.setAccessible(true);
      Method addOtherSide = thisSideClass.getMethod(Composer.composeAdder(otherSideClass), otherSideClass);
      Method clear = List.class.getMethod("clear", (Class[]) null);
      Method size = List.class.getMethod("size", (Class[]) null);
      Method get = List.class.getMethod("get", int.class);
      Field remoteLocalSideField = otherSideClass.getDeclaredField(Composer.getFieldName(thisSideClass));
      remoteLocalSideField.setAccessible(true);
      
      //de-reference other side:
      Integer listSize = (Integer) size.invoke(localOtherSideField.get(thisSide), (Object[]) null);
      for (int i = 0; i < listSize; i++) {
      	remoteLocalSideField.set(get.invoke(localOtherSideField.get(thisSide), i), null);
      }
      
      //clear 
      clear.invoke(localOtherSideField.get(thisSide), (Object[]) null);
      for (Object object : otherSide) {
        addOtherSide.invoke(thisSide, object);
      }
    } catch (Exception e) {
      logger.error("Error while wiring collection side on a 1 to M bidirectional relationship", e);
    }
  }
  
  /**
   * <pre>
   * Wires the Many To One bidirectional relationship, by means of addition.
   * 
   * For the Customer <-> Invoice use case, you use this method in the adder
   * on the M sided entity, in this case:
   * 
   * In the Customer class:
   * 
   * public void addInvoice(Invoice invoice) {
   *   OneToManyUtil.wireAddition(this, invoice, Customer.class, Invoice.class);
   * }
   * </pre>
   * 
   * @param thisSide this side of the M to 1 relationship
   * @param otherSide the other side of the M to 1 relationship
   * @param thisSideClass the class on this side of the relationship
   * @param otherSideClass the class on the other side of the relationship
   */
  public static void wireAddition(Object thisSide, Object otherSide, Class thisSideClass, Class otherSideClass) {
    try {
      Field localOtherSideField = thisSideClass.getDeclaredField(Composer.composeCollectionFieldName(otherSideClass));
      localOtherSideField.setAccessible(true);
      Method setThisSide = otherSideClass.getMethod(Composer.composeSetter(thisSideClass), thisSideClass);
      Method add = List.class.getMethod("add", Object.class);
      Method contains = List.class.getMethod("contains", Object.class);
      
      Boolean b = (Boolean) contains.invoke(localOtherSideField.get(thisSide), otherSide);
      if (!b) {
        add.invoke(localOtherSideField.get(thisSide), otherSide);
        setThisSide.invoke(otherSide, thisSide);
      }      
    } catch (Exception e) {
      logger.error("Error while wiring addition on a 1 to M bidirectional relationship", e);
    }
  }
  
  /**
   * <pre>
   * Wires the Many to One bidirectional relationship, by means of removal.
   * 
   * For the Customer <-> Invoice use case, you use this method in the remover
   * on the M sided entity, in this case:
   * 
   * In the Customer class:
   * 
   * public void removeInvoice(Invoice invoice) {
   *   OneToManyUtil.wireRemoval(this, invoice, Customer.class, Invoice.class);
   * }
   * </pre>
   * 
   * @param thisSide this side of the M to 1 relationship
   * @param otherSide the other side of the M to 1 relationship
   * @param thisSideClass the class on this side of the relationship
   * @param otherSideClass the class on the other side of the relationship
   */
  public static void wireRemoval(Object thisSide, Object otherSide, Class thisSideClass, Class otherSideClass) {
    try {
      Field localOtherSideField = thisSideClass.getDeclaredField(Composer.composeCollectionFieldName(otherSideClass));
      localOtherSideField.setAccessible(true);
      Method setThisSide = otherSideClass.getMethod(Composer.composeSetter(thisSideClass), thisSideClass);
      Method remove = List.class.getMethod("remove", Object.class);
      Method contains = List.class.getMethod("contains", Object.class);
      
      Boolean b = (Boolean) contains.invoke(localOtherSideField.get(thisSide), otherSide);
      if (b) {
        remove.invoke(localOtherSideField.get(thisSide), otherSide);
        Object n = null;
        setThisSide.invoke(otherSide, n);
      }
    } catch (Exception e) {
      logger.error("Error while wiring removal on a 1 to M bidirectional relationship", e);
    }
  }
  
}
